﻿#include "Chunk.h"

#include "App/Game.h"
#include "Render/BlockMesh.h"
#include "World/BlockRegistry.h"
#include "World/World.h"

#include <Frustum.h>

// temp
static const std::array<std::array<glm::ivec3, 6>, 6> offsetsToCheck =
    {
        std::array<glm::ivec3, 6>{
            // 朝西时
            glm::ivec3{0, 0, -1},
            glm::ivec3{0, 0, 1},
            glm::ivec3{0, 1, 0},
            glm::ivec3{0, -1, 0},
            glm::ivec3{-1, 0, 0},
            glm::ivec3{1, 0, 0},
        },
        std::array<glm::ivec3, 6>{
            // 朝东时
            glm::ivec3{0, 0, 1},
            glm::ivec3{0, 0, -1},
            glm::ivec3{0, 1, 0},
            glm::ivec3{0, -1, 0},
            glm::ivec3{1, 0, 0},
            glm::ivec3{-1, 0, 0},
        },
        {
            // 朝上时
            glm::ivec3{1, 0, 0},
            glm::ivec3{-1, 0, 0},
            glm::ivec3{0, 1, 0},
            glm::ivec3{0, -1, 0},
            glm::ivec3{0, 0, 1},
            glm::ivec3{0, 0, -1},
        },
        {
            // 朝下时
            glm::ivec3{1, 0, 0},
            glm::ivec3{-1, 0, 0},
            glm::ivec3{0, 1, 0},
            glm::ivec3{0, -1, 0},
            glm::ivec3{0, 0, 1},
            glm::ivec3{0, 0, -1},
        },
        std::array<glm::ivec3, 6> //
        {
            // 朝北时
            glm::ivec3{1, 0, 0},
            glm::ivec3{-1, 0, 0},
            glm::ivec3{0, 1, 0},
            glm::ivec3{0, -1, 0},
            glm::ivec3{0, 0, 1},
            glm::ivec3{0, 0, -1},
        },
        std::array<glm::ivec3, 6>{
            // 朝南时
            glm::ivec3{-1, 0, 0},
            glm::ivec3{1, 0, 0},
            glm::ivec3{0, 1, 0},
            glm::ivec3{0, -1, 0},
            glm::ivec3{0, 0, -1},
            glm::ivec3{0, 0, 1},
        },
};

bool HasNonTransparentAt(const glm::ivec3 &pos, const Chunk &chunk, const World &world)
{
  const BlockData *block = chunk.GetBlockAtOptimized(pos, world);
  if (block != nullptr && block->id != 0)
  {
    Ref<BlockRegistry> blockReg = Game::GetInstance().GetLevel()->GetBlockRegistry();
    bool isTransparent = blockReg->GetBlock(block->id)->renderClass == Block::RenderType::Transparent;
    return !isTransparent;
  }
  return false;
}

uint8_t CalculateOcclusionLevel(const glm::ivec3 &blockPos,
                                const glm::ivec3 &vertOffset,
                                const Chunk &chunk,
                                const World &world)
{
  glm::ivec3 direction = glm::sign(glm::vec3(vertOffset) - glm::vec3(.5));

  uint8_t side1 = HasNonTransparentAt(blockPos + direction * glm::ivec3(1, 1, 0), chunk, world) ? 1 : 0;
  uint8_t side2 = HasNonTransparentAt(blockPos + direction * glm::ivec3(0, 1, 1), chunk, world) ? 1 : 0;
  if (side1 && side2)
  {
    return 0;
  }

  uint8_t corner = HasNonTransparentAt(blockPos + direction * glm::ivec3(1, 1, 1), chunk, world) ? 1 : 0;
  return 3 - (side1 + side2 + corner);
}

Chunk::Chunk(const glm::ivec2 &worldPosition)
    : m_worldPosition(worldPosition), m_aabb(glm::vec3(0), glm::vec3(0))
{
  Init();
}

void Chunk::Init()
{
  m_solidVertexCount = 0;
  m_semiTransparentVertexCount = 0;
  m_mesh = nullptr;
  m_renderState = RenderState::Init;

  glm::vec3 position = glm::vec3(m_worldPosition.x, 0, m_worldPosition.y);
  glm::vec3 maxOffset = glm::vec3(HorizontalSize, VerticalSize, HorizontalSize);
  m_aabb = AABB{position, position + maxOffset};

  memset(vertexLightData, 0, sizeof(vertexLightData));
}

void Chunk::RebuildMesh(const World &world)
{
  const auto level = Game::GetInstance().GetLevel();
  if (level == nullptr)
    return;

  LOG_TRACE("Rebuilding chunk: ({}, {})", m_worldPosition.x, m_worldPosition.y);

  static Ref<std::vector<BlockVertex>> solidVertices = CreateScoped<std::vector<BlockVertex>>(MaxVertexCount);
  static Ref<std::vector<BlockVertex>> semiTransparentVertices = CreateScoped<std::vector<BlockVertex>>(MaxVertexCount);
  m_solidVertexCount = 0;
  m_semiTransparentVertexCount = 0;

  Ref<BlockRegistry> blockReg = level->GetBlockRegistry();

  for (int32_t x = HorizontalSize - 1; x >= 0; --x)
  {
    for (int32_t y = VerticalSize - 1; y >= 0; --y)
    {
      for (int32_t z = HorizontalSize - 1; z >= 0; --z)
      {
        glm::ivec3 blockPos{x, y, z};
        BlockData &blockData = data[x][y][z];
        Block *block = blockReg->GetBlock(blockData.id);
        if (block == nullptr)
          continue;

        if (block->renderClass == Block::RenderType::Air)
          continue;

        Ref<BlockModel> model = block->model;
        if (model == nullptr)
          continue;

        for (int offsetIndex = 0; offsetIndex < 6; offsetIndex++)
        {
          const auto &offset = offsetsToCheck[blockData.direction][offsetIndex];
          const glm::ivec3 neighborPos = blockPos + offset;
          const BlockData *neighbor = GetBlockAtOptimized(neighborPos, world);
          if (neighbor != nullptr)
          {
            Block *neighborBlock = blockReg->GetBlock(neighbor->id);
            if (neighborBlock == nullptr)
              continue;

            if (block->modelType != Block::BlockModelType::NotFull)
            {
              // 如果是非完整方块，都要渲染 // TODO：后续优化成重合面不渲染
              const bool isSameClass = neighborBlock->renderClass == block->renderClass;
              const bool isTransparentNextToOpaque =
                  neighborBlock->renderClass == Block::RenderType::Opaque && block->renderClass == Block::RenderType::Transparent;
              const bool neighborIsNotFull = neighborBlock->modelType == Block::BlockModelType::NotFull;
              if (!neighborIsNotFull && (isSameClass || isTransparentNextToOpaque))
                continue;
            }
          }

          std::vector<BlockVertex> &vertices = model->GetVerticesFromDirection(offset);
          for (auto vert : vertices)
          {
            // 光照
            const glm::ivec3 &relativePos = neighborPos + glm::ivec3(vert.position);
            if (IsValidPosition(relativePos))
            {
              const uint8_t lightLevel = vertexLightData[relativePos.x][relativePos.y][relativePos.z];
              vert.SetVertexLight(lightLevel);
            }
            else
            {
              const auto worldPos = relativePos + glm::ivec3(m_worldPosition.x, 0, m_worldPosition.y);
              auto neighborChunk = world.GetChunk(World::GetChunkIndex(worldPos));
              if (neighborChunk)
              {
                const auto inChunkPos = World::ToPositionInChunk(worldPos);
                const uint8_t lightLevel = neighborChunk->vertexLightData[inChunkPos.x][inChunkPos.y][inChunkPos.z];
                vert.SetVertexLight(lightLevel);
              }
            }

            vert.position += glm::vec3(x, y, z);

            if (block->model->isAmbientOcclusion)
            {
              // 封闭性
              uint8_t occlusionLevel = 3;
              if (m_useAmbientOcclusion)
              {
                if (offset.y == -1)
                {
                  occlusionLevel = 0;
                }
                else
                {
                  occlusionLevel = CalculateOcclusionLevel(blockPos, vert.GetPosition() - blockPos, *this, world);
                }
              }

              vert.SetOcclusionLevel((uint8_t)occlusionLevel);
            }

            if (block->renderClass == Block::RenderType::SemiTransparent ||
                block->renderClass == Block::RenderType::Transparent)
            {
              semiTransparentVertices->at(m_semiTransparentVertexCount) = vert;
              m_semiTransparentVertexCount++;
            }
            else
            {
              solidVertices->at(m_solidVertexCount) = vert;
              m_solidVertexCount++;
            }
          }
        }
      }
    }
  }
  int32_t vertexCount = m_solidVertexCount + m_semiTransparentVertexCount;

  if (!m_mesh)
  {
    m_mesh = CreateScoped<VertexArray>();
    m_mesh->AddVertexAttributes(BlockVertex::VertexAttributes(), sizeof(BlockVertex));
  }

  Ref<VertexBuffer> buffer = m_mesh->GetVertexBuffer();
  if (buffer->GetSize() < vertexCount)
  {
    int32_t dataSize = glm::min(vertexCount + 1000, MaxVertexCount);
    buffer->BufferDynamicData(*solidVertices, dataSize, 0);
  }
  else
  {
    buffer->BufferDynamicSubData(*solidVertices, m_solidVertexCount, 0, 0);
  }

  buffer->BufferDynamicSubData(*semiTransparentVertices, m_semiTransparentVertexCount, 0, m_solidVertexCount);

  m_renderState = RenderState::Ready;
}

bool Chunk::IsValidPosition(const glm::ivec3 &chunkPosition)
{
  return chunkPosition.x >= 0 && chunkPosition.x < HorizontalSize &&
         chunkPosition.y >= 0 && chunkPosition.y < VerticalSize &&
         chunkPosition.z >= 0 && chunkPosition.z < HorizontalSize;
}

bool Chunk::PlaceBlock(const glm::ivec3 &chunkPosition, BlockData block)
{
  if (!IsValidPosition(chunkPosition))
    return false;

  m_renderState = RenderState::Dirty;
  data[chunkPosition.x][chunkPosition.y][chunkPosition.z] = block;

  return true;
}

BlockData *Chunk::GetBlockAt(const glm::ivec3 &chunkPosition)
{
  if (!IsValidPosition(chunkPosition))
    return nullptr;

  return &data[chunkPosition.x][chunkPosition.y][chunkPosition.z];
}

float Chunk::Distance2DTo(const glm::vec2 &point) const
{
  glm::vec2 referencePoint = {glm::clamp(point.x, (float)m_worldPosition.x, (float)m_worldPosition.x + 16.0f),
                              glm::clamp(point.y, (float)m_worldPosition.y, (float)m_worldPosition.y + 16.0f)};

  return glm::distance(referencePoint, point);
}

void Chunk::RenderOpaque(const Ref<Camera> &camera)
{
  if (!m_mesh || !m_shader)
    return;

  Frustum frustum(camera->GetViewProjectionMatrix());
  if (!frustum.IsBoxVisible(m_aabb.minPoint, m_aabb.maxPoint))
    return;

  m_shader->Bind();
  const auto model = glm::translate(glm::vec3(m_worldPosition.x, 0, m_worldPosition.y));
  m_shader->SetMat4("uMVP", camera->GetViewProjectionMatrix() * model);
  m_shader->SetMat4("uView", camera->GetViewMatrix());
  m_shader->SetMat4("uModel", model);
  if (m_solidVertexCount != 0)
    m_mesh->DrawVertexSub(m_solidVertexCount, 0);
}

void Chunk::RenderSemiTransparent(const Ref<Camera> &camera)
{
  if (!m_mesh || !m_shader)
    return;

  Frustum frustum(camera->GetViewProjectionMatrix());
  if (!frustum.IsBoxVisible(m_aabb.minPoint, m_aabb.maxPoint))
    return;

  m_shader->Bind();
  m_shader->SetMat4("uMVP", camera->GetViewProjectionMatrix() * glm::translate(glm::vec3(m_worldPosition.x, 0, m_worldPosition.y)));

  glDisable(GL_CULL_FACE);
  if (m_semiTransparentVertexCount != 0)
    m_mesh->DrawVertexSub(m_semiTransparentVertexCount, m_solidVertexCount);
  glEnable(GL_CULL_FACE);
}

const BlockData *Chunk::GetBlockAtOptimized(const glm::ivec3 &pos, const World &world) const
{
  const glm::ivec2 &worldPos = m_worldPosition;
  if (pos.y >= 0 && pos.y < Chunk::VerticalSize)
  {
    if (pos.x >= 0 && pos.x < Chunk::HorizontalSize && pos.z >= 0 && pos.z < Chunk::HorizontalSize)
    {
      return &data[pos.x][pos.y][pos.z];
    }
    else
    {
      return world.GetBlockAtIfLoaded(glm::ivec3(pos.x + worldPos.x, pos.y, pos.z + worldPos.y));
    }
  }

  return nullptr;
}
